iT邦幫忙

2021 iThome 鐵人賽

DAY 6
1
自我挑戰組

從C到JS的同步非同步探索系列 第 6

[Day 6] .Net WhenAll 底層(1)

  • 分享至 

  • xImage
  •  

前言

這系列教學文的目的是要探索具備非同步功能的框架在底層發生了什麼事, 甚至寫一個簡單的框架出來, 目前我們終於進展到閱讀我們的第一個目標 .Net 。 之所以選擇 .Net 作為開始, 一是因為我 C# 比較熟, 二是因為他們擁有很好的文件和索引, 三是因為他們的封裝邏輯很棒(個人感受), 那我們就一起來看看吧。

先備知識

API 文件

https://docs.microsoft.com/zh-tw/dotnet/api/system.threading.tasks.task.whenall?view=net-5.0

怕大家不懂 C# , 這邊說明一下, Task.WhenAll 的功能和 JS 的 Promise.all 差不多。

說白了就是停下來等待一堆非同步工作的執行。

基本用法

要先包裝出多個非同步工作, 接著把這些工作當參數傳入 WhenAll, 最後 WhenAll 會回傳執行結果。這個方法應該是 C# 使用端在實踐非同步 programming 得常見做法了吧。

本篇文章參考

.Net reference source

https://referencesource.microsoft.com/#mscorlib/system/threading/Tasks/Task.cs,5955

先看註解

///
/// Creates a task that will complete when all of the supplied tasks have completed.
///
/// The tasks to wait on for completion.
/// A task that represents the completion of all of the supplied tasks.
///
///
/// If any of the supplied tasks completes in a faulted state, the returned task will also complete in a Faulted state,
/// where its exceptions will contain the aggregation of the set of unwrapped exceptions from each of the supplied tasks.
///
///
/// If none of the supplied tasks faulted but at least one of them was canceled, the returned task will end in the Canceled state.
///
///
/// If none of the tasks faulted and none of the tasks were canceled, the resulting task will end in the RanToCompletion state.
///
///
/// If the supplied array/enumerable contains no tasks, the returned task will immediately transition to a RanToCompletion
/// state before it's returned to the caller.
///

可以知道

  • 總結 : 創建一個將在所有提供的任務完成後完成的任務。
  • 參數 : 已被包裝好等待完成的非同步任務列表
  • 回傳 : 任務列表完成的狀態

看 source code

我們要讀的是這個 overload public static Task WhenAll(params Task[] tasks)

public static Task WhenAll(params Task[] tasks)
{
    // Do some argument checking and make a defensive copy of the tasks array
    if (tasks == null) throw new ArgumentNullException("tasks");
    Contract.EndContractBlock();

    int taskCount = tasks.Length;
    if (taskCount == 0) return InternalWhenAll(tasks); // Small optimization in the case of an empty array.
    Task[] tasksCopy = new Task[taskCount];
    for (int i = 0; i < taskCount; i++)
    {
        Task task = tasks[i];
        if (task == null) throw new ArgumentException(Environment.GetResourceString("Task_MultiTaskContinuation_NullTask"), "tasks");
        tasksCopy[i] = task;
    }
    // 以上例外處理, 跳過。
    // The rest can be delegated to InternalWhenAll()
    return InternalWhenAll(tasksCopy);
}

可以發現, 其複製了一份傳入參數, 丟入 InternalWhenAll(tasksCopy) , 代其回傳。

接著我們讀一下 InternalWhenAll(tasksCopy)

private static Task InternalWhenAll(Task[] tasks)
{
    Contract.Requires(tasks != null, "Expected a non-null tasks array");
    return (tasks.Length == 0) ? // take shortcut if there are no tasks upon which to wait
        Task.CompletedTask :
        new WhenAllPromise(tasks);
}

發現他除了例外處理以外, 又往下丟一層, 進入 WhenAllPromise(tasks)

internal WhenAllPromise(Task[] tasks) : base()
{
    Contract.Requires(tasks != null, "Expected a non-null task array");
    Contract.Requires(tasks.Length > 0, "Expected a non-zero length task array");

    if (AsyncCausalityTracer.LoggingOn)
        AsyncCausalityTracer.TraceOperationCreation(CausalityTraceLevel.Required, this.Id, "Task.WhenAll", 0);

    if (s_asyncDebuggingEnabled)
    {
        AddToActiveTasks(this);
    }

    m_tasks = tasks;
    // 記下任務總數
    m_count = tasks.Length;

    foreach (var task in tasks)
    {
        // 當這個任務被判斷為已經完成, 則走捷徑
        if (task.IsCompleted) this.Invoke(task); // short-circuit the completion action, if possible
        else task.AddCompletionAction(this); // simple completion action
    }
}

一般來說要看一下繼承, 但 base() 是空的, 跳過。

跳過 log 相關, debug 相關, 我們可以發現兩條核心方法, this.Invoke(task)task.AddCompletionAction(this) 兩條, 但從註解可以看出, 走InvokeAddCompletionAction 的捷徑, 我們先讀Invoke。 這裡的 this 指的是等待所有任務完成的任務, 就是本 method 的調用者。

public void Invoke(Task completedTask)

https://referencesource.microsoft.com/#mscorlib/system/threading/Tasks/Task.cs,6123

較長, 可以直接到 reference 看, 這裡擷取語句

// atomic 的把 task 的數量減一, 如果減完是 0 表示任務全部完成
if (Interlocked.Decrement(refm_count) == 0)
{
    // 設定回傳結果, 略
}

我們整理一下 當 whenAll 開始運行, 會先取得傳入的 task 總數, 接著依序對傳入的 task 判斷狀態, 若是判斷為 task 已完成時, 會把 task 總數減1, 當總數被減為 0 時, 會設定回傳結果且回傳。

Interlocked.Decrement 文件

https://docs.microsoft.com/zh-tw/dotnet/api/system.threading.interlocked.decrement?view=net-5.0

明天進度

今天我們留下了兩個疑問, 要交給明天的我解決。

  1. 為何要 "atomic" 的把 task 的總數減一, 看起來只有一條 thread 會動到啊 ?
  2. 如果 WhenAll 在查看 task 狀態時, task 未完成會進入另一條路線, 那條路線發生了什麼呢 ?

明天見!


上一篇
[Day 5] lock-free stack
下一篇
[Day 7] .Net WhenAll 底層(2)
系列文
從C到JS的同步非同步探索30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言